package pl.enigmatic.parser.xml;

import static pl.enigmatic.literal.collection.Array;

import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import pl.enigmatic.math.MarkedSubrangeList;
import pl.enigmatic.math.OrderedMap;
import pl.enigmatic.reflection.Methods;
import pl.enigmatic.util.Reflection;
import pl.enigmatic.util.Strings;
import pl.enigmatic.util.XML;
import pl.enigmatic.parser.XMLContent;
import pl.enigmatic.parser.XMLExtension;
import pl.enigmatic.parser.XMLGetter;
import pl.enigmatic.parser.XMLMarkup;
import pl.enigmatic.parser.XMLModule;
import pl.enigmatic.parser.XMLNames;
import pl.enigmatic.parser.XMLPackage;
import pl.enigmatic.parser.XMLParser;
import pl.enigmatic.parser.XMLProperties;
import pl.enigmatic.parser.markup.BasicMarkupExtension;
import pl.enigmatic.parser.markup.IncludeExtension;
import pl.enigmatic.parser.markup.MarkupExtension;
import pl.enigmatic.parser.markup.MarkupProperties;
import pl.enigmatic.parser.markup.NullExtension;
import pl.enigmatic.parser.markup.ReferenceExtension;
import pl.enigmatic.parser.markup.StaticExtension;
import pl.enigmatic.parser.metadata.Metadata;
import pl.enigmatic.parser.metadata.MetadataParserExtension;
import pl.enigmatic.parser.metadata.NameExtension;
import pl.enigmatic.parser.metadata.ValueExtension;
import pl.enigmatic.parser.string.Converter;


/**
 * Class for parsing resources stored in xml into objects in java.
 * @author reggie_7
 * @param <T> indicates what type of object will be returned by a specific instance of this class
 */
@SuppressWarnings("unchecked")
public class Parser<T> implements XMLParserExtension, PropertySetter, PropertiesNamesMap, ObjectCreator<T>, PropertyGetter {

	public static final String XMLNS_PREFIX = "http://www.enigma.edu.pl/";

	public static final String XMLNS(final String... path) {
		final StringBuilder builder = new StringBuilder();
		builder.append(XMLNS_PREFIX);
		for (final String str : path) {
			builder.append(str);
			builder.append("/");
		}
		return builder.toString();
	}

	static {
		DOMConfigurator.configure("log4j.xml");
	}

	/** logging utility object */
	private static Logger logger = Logger.getLogger(Parser.class);

	/** a collection of parsers maintained to keep the ones already created */
	private static HashMap<String, Parser<?>> parsers = new HashMap<String, Parser<?>>();

	static {
		parsers.put(null, createParser(Object.class));
		for (final Class<?> cls : Array(Byte.TYPE, Byte.class, Short.TYPE, Short.class, Integer.TYPE, Integer.class, Long.TYPE, Long.class,
				Float.TYPE, Float.class, Double.TYPE, Double.class, Character.TYPE, Character.class, Boolean.TYPE, Boolean.class, String.class)) {
			parsers.put(cls.getName(), createParser(cls));
		}
	}

	private static void error(final Throwable t, final String format, final Object... args) {
		logger.error(String.format(format, args), t);
	}

	// ========================================================================================================================================================
	// static parsing stuff
	// ========================================================================================================================================================

	@SuppressWarnings({ "rawtypes" })
	private static <X> X parseXML(final X target, final Document doc, final String path) {
		final Element xml = doc.getDocumentElement();
		final StaticParsingInfo staticInfo = XMLNSRetriever.clean(new StaticParsingInfo(xml, ParsingMetadata.clean(doc), path), doc);
		final String clazz;
		if (target != null) {
			staticInfo.add(target.getClass().getPackage().getName());
			clazz = target.getClass().getName();
		} else {
			clazz = NodeNameInfo.getFullClassName(xml, staticInfo);
		}
		if (clazz != null) {
			final Parser parser = createParser(clazz);
			if (parser != null) {
				final ParsingInfo info = ParsingInfo.create(parser, staticInfo, new NodeNameInfo(null, clazz), xml);
				return (X) (target == null ? parser.parse(info) : parser.parse(info, target));
			} else {
				error(new Exception("XML syntax error probably"), "No parser found for class %1$s.", clazz);
			}
		} else {
			error(new Exception("XML syntax error"), "XML syntax error for name %1$s. It should be a class declaration.", xml.getTagName());
		}
		return null;
	}

	public static <X> X parseXML(final X target, final InputStream is, final String path) {
		return parseXML(target, XML.parseStream(is), path);
	}

	public static <X> X parseXML(final X target, final XMLResourceLocator resolver, final String path) {
		return parseXML(target, resolver.getXML(path), path);
	}

	public static <X> X parseXML(final InputStream is, final String path) {
		return parseXML(null, is, path);
	}

	public static <X> X parseXML(final XMLResourceLocator resolver, final String path) {
		return parseXML(null, resolver, path);
	}

	public static boolean initializeObject(final Object object) {
		try {
			final Class<?> cls = object.getClass();
			final String xml = cls.getSimpleName() + ".xml";
			final URL url = cls.getResource(xml);
			if (url != null) {
				// TODO sprawdzi� poprawno�� �cie�ki
				parseXML(object, url.openStream(), xml);
				return true;
			}
		} catch (final Exception e) {
			error(e, "Object initialization error.");
		}
		return false;
	}

	/**
	 * Parses the given xml <code>node</code> element to the given <code>target</code> object. Following are the rules of xml syntax:
	 * <hr/>
	 * <h1>Namespace rules</h1>
	 * <hr/>
	 * <ol>
	 * 
	 * <li>The xml namespace defines the default package in which the parsing process will try to find defined elements. The namespace declaration
	 * should be of the following form:<br>
	 * <strong>http://package.extension/&lt;here the proper package name&gt;</strong></li>
	 * <li>If the name of an element or an attribute is prefixed with a custom namespace prefix, then the custom package is applied, otherwise - the
	 * default one will be taken into account.</li>
	 * <li>The default xml namespace <strong>must be</strong> supplied as an attribute of the root element, at least in the minimal form, which is the
	 * following:<br>
	 * <strong>xmlns="http://package.extension/"</strong></li>
	 * <li>There is a metadata namespace, of the following, preferable form:<br>
	 * <strong>xmlns:metadata="http://metadata.extension/"</strong><br>
	 * which is reserved for special purposes keywords, which can be found {@link Metadata here}</li>
	 * <li>The parser replaces all '/' occurences with dots, so the following namespace declarations will result in the same package:
	 * <ul>
	 * <li>xmlns:custom="http://package.extension/my/package/declaration"</li>
	 * <li>xmlns:custom="http://package.extension/my.package.declaration"</li>
	 * </ul>
	 * </li>
	 * <li>The root element of a document must always define Java class.</li>
	 * </ol>
	 * 
	 * 
	 * 
	 * <hr/>
	 * <h1>Java language conventions</h1>
	 * <hr/>
	 * 
	 * For the parsing algorithm to work correctly the programmer must follow these rules of code syntax:
	 * <ol>
	 * <li>All classes supposed to be parsed must have names starting with uppercase letter.</li>
	 * <li>All field members must start with lowercase letter.</li>
	 * <li>All package name parts (separated by dots) must start with lowercase letter.</li>
	 * </ol>
	 * 
	 * 
	 * 
	 * <hr/>
	 * <h1>XML conventions</h1>
	 * <hr/>
	 * 
	 * <h1>Nodes</h1>
	 * <ol>
	 * <li>To define a class (eg. my.own.package.MyClass) element within xml file we can follow one of patterns :
	 * <ul>
	 * <li>&lt;my.own.package.MyClass&gt;...</li>
	 * <li>&lt;custom:MyClass&gt;... if we defined earlier appropriate namespace: xmlns:custom="http://package.extension/my/own/package"</li>
	 * <li>&lt;MyClass&gt;... if we defined earlier appropriate namespace: xmlns="http://package.extension/my/own/package"</li>
	 * </ul>
	 * </li>
	 * <li>To define an inner class we can follow this example: &lt;OuterClass.InnerClass&gt;, following also general rules concerning simple classes.
	 * </li>
	 * <li>To define a dependency property to be set on an object we define a node &lt;MyDependencyObjectClass.myDependencyPropertyName&gt; where
	 * MyDependencyObjectClass identifies the property defining class and myDependencyPropertyName is the string passed as a parameter to registering
	 * method, eg. to {@link DependencyProperty#register(String, Class, Class)}. MyDependencyObjectClass class definition additionally follows the
	 * rules given above, for general class definitions.</li>
	 * <li>To set a myProperty property being a simple field member of an object, or being registered inside the object class as a dependency property
	 * we follow simple node syntax rule: &lt;myProperty&gt;</li>
	 * </ol>
	 * 
	 * <h1>Attributes</h1>
	 * 
	 * 
	 * <hr/>
	 * 
	 * @param path the path to document being parsed - can be relative or absolute (the latter must start with '/')
	 * @return object representing parsed xml document
	 */
	public static <X> X parseXML(final String path) {
		return parseXML(new XMLResourceLocator.BasicImpl(), path);
	}

	public static class XMLParserWrapper {

		private final XMLResourceLocator resolver;

		public XMLParserWrapper(final XMLResourceLocator resolver) {
			this.resolver = resolver;
		}

		public <X> X parse(final String path) {
			return parseXML(resolver, path);
		}
	}

	/**
	 * Creates a parser for a given class type.
	 * @param <T> class type parameter
	 * @param clazz class type
	 * @return parser for a class type given
	 */
	@SuppressWarnings({ "rawtypes" })
	public static <T> Parser<T> createParser(final Class<T> clazz) {
		String name = null;
		if (clazz != null) {
			name = clazz.getName();
		}
		if (!parsers.containsKey(name)) {
			Parser<T> parser = null;
			if (clazz.isEnum()) {
				parser = new EnumParser(clazz);
			} else if (clazz.isArray()) {
				if (Reflection.isStrictPrimitive(Reflection.arrayBase(clazz))) {
					parser = new PrimitiveArrayParser(clazz);
				} else {
					parser = new ArrayParser(clazz);
				}
			} else if (clazz.isInterface()) {
				parser = (Parser<T>) parsers.get(Object.class.getName());
			} else {
				Class<? extends Parser<T>> c = null;
				if (clazz.isAnnotationPresent(XMLParser.class)) {
					c = (Class<? extends Parser<T>>) clazz.getAnnotation(XMLParser.class).value();
				} else {
					final String custom = Parser.class.getPackage().getName() + "." + Reflection.wrapper(clazz).getName()
							+ Parser.class.getSimpleName();
					try {
						c = (Class<? extends Parser<T>>) Reflection.classForName(custom);
					} catch (final Exception e) {
						c = null;
						error(e, "Wrong superclass for custom parser %1$s.", custom);
					}
				}
				if (c != null) {
					try {
						parser = c.newInstance();
					} catch (final Exception e) {
						error(e, "Unable to instantiate custom parser %1$s.", c.getName());
					}
				} else {
					parser = new Parser<T>(clazz, true);
				}
			}
			parsers.put(name, parser);
		}
		return (Parser<T>) parsers.get(name);
	}

	/**
	 * Creates a parser for a given class type name.
	 * @param <T> class type parameter
	 * @param clazz class type name
	 * @return parser for a class type name given
	 */

	public static <T> Parser<T> createParser(final String clazz) {
		if (!parsers.containsKey(clazz)) {
			return createParser((Class<T>) Reflection.classForName(clazz));
		} else {
			return (Parser<T>) parsers.get(clazz);
		}
	}

	public static Parser<?> createCustomParser(final Class<Parser<?>> clazz) {
		final String name = clazz.getName();
		if (!parsers.containsKey(name)) {
			try {
				parsers.put(name, clazz.newInstance());
			} catch (final Exception e) {
				error(e, "Unable to instantiate custom parser %1$s.", name);
			}
		}
		return parsers.get(name);
	}

	// ========================================================================================================================================================
	// initialization stuff
	// ========================================================================================================================================================

	/** type for parsing by this parser */
	protected final Class<T> clazz;
	/** default converter */
	private final Converter<T> converter;
	/** parser for super class */
	private final Parser<? super T> parent;
	/** list of objects handling various metadata nodes parsing process */
	private final Map<Metadata, MetadataParserExtension> metadata = new HashMap<Metadata, MetadataParserExtension>();
	// private final List<ObjectCreator<T>> creators = new Vector<ObjectCreator<T>>();
	private final OrderedMap<Class<? extends ObjectCreator<T>>, ObjectCreator<T>, ?> creators = new OrderedMap.VectorImpl<Class<? extends ObjectCreator<T>>, ObjectCreator<T>>();
	/** list of objects handling various nodes parsing process */
	private final MarkedSubrangeList<XMLParserExtension> extensions = new MarkedSubrangeList<XMLParserExtension>();
	private final MarkedSubrangeList<PropertyGetter> getters = new MarkedSubrangeList<PropertyGetter>();
	private final MarkedSubrangeList<MarkupExtension> markups = new MarkedSubrangeList<MarkupExtension>();
	private final List<String> packages = new Vector<String>();
	private final Field properties;

	/** content accessor */
	private final PropertySetter content;
	private final PropertiesNamesMap propertiesNamesMap;

	// TODO zamienić używanie parsera na interfejs tak, żeby uniknąć konieczności używania parametru dla pełnej inicjalizacji
	/**
	 * @param cls the <code>class</code> type which <code>this</code> parser will be specified for
	 * @param fullInitialization indicates whether to fully initialize all available components of the parser (extending classes will usually use
	 *        <code>false</code> value here)
	 */

	protected Parser(final Class<T> cls, final boolean fullInitialization) {
		parsers.put(cls.getName(), this);
		clazz = cls;
		final Class<? super T> superclass = clazz.getSuperclass();
		parent = fullInitialization && superclass != null ? createParser(superclass) : null;
		converter = createConverter();
		addCreator(this);
		propertiesNamesMap = initPropertyNamesMap();

		// extensions
		addMetadataExtension(NameExtension.instance);
		if (fullInitialization) {
			// addMetadataExtension(ValueExtension.instance);
			addExtension(new CollectionPropertyExtension(this));
			addExtension(new MapPropertyExtension(this));
			addExtension(new PropertyParser(this));
			addExtension(new PublicPropertyParser(this));
			addCreator(addExtension(new FinalPropertyParser(this)));
			addCreator(addExtension(new FinalPublicPropertyParser(this)));
			addCreator(ValueExtension.instance);
			addExtension(new NestedPropertyExtension(this));
			addGetter(new SimplePropertyGetter(this));
			addGetter(new PublicPropertyGetter(this));
			addMarkup(ReferenceExtension.instance);
			addMarkup(IncludeExtension.instance);
			addMarkup(NullExtension.instance);
			addMarkup(StaticExtension.instance);
		}
		extensions.useRelativeOffsets(extensions.size(), 1);
		getters.useRelativeOffsets(getters.size(), 0);
		markups.useRelativeOffsets(markups.size(), 0);

		if (fullInitialization) {
			final Map<Class<?>, Object> map = new HashMap<Class<?>, Object>();
			for (final XMLParserExtension ext : createFromParentAndAnnotation(hasParent() ? parent.extensions : null, clazz
					.isAnnotationPresent(XMLExtension.class) ? clazz.getAnnotation(XMLExtension.class).value() : null, map)) {
				addExtension(ext);
			}
			for (final PropertyGetter ext : createFromParentAndAnnotation(hasParent() ? parent.getters : null, clazz
					.isAnnotationPresent(XMLGetter.class) ? clazz.getAnnotation(XMLGetter.class).value() : null, map)) {
				addGetter(ext);
			}
			for (final MarkupExtension ext : createFromParentAndAnnotation(hasParent() ? parent.markups : null, clazz
					.isAnnotationPresent(XMLMarkup.class) ? clazz.getAnnotation(XMLMarkup.class).value() : null, map)) {
				addMarkup(ext);
			}
			if (clazz.isAnnotationPresent(XMLModule.class)) {
				final XMLModule ann = clazz.getAnnotation(XMLModule.class);
				final Object module = createObjectFromClass(ann.value(), true);
				if (module != null) {
					for (final Class<?> c : ann.modules()) {
						if (c.equals(MetadataParserExtension.class) && module instanceof MetadataParserExtension) {
							addMetadataExtension((MetadataParserExtension) module);
						} else if (c.equals(ObjectCreator.class) && module instanceof ObjectCreator) {
							addCreator((ObjectCreator<T>) module);
						} else if (c.equals(XMLParserExtension.class) && module instanceof XMLParserExtension) {
							addExtension((XMLParserExtension) module);
						} else if (c.equals(PropertyGetter.class) && module instanceof PropertyGetter) {
							addGetter((PropertyGetter) module);
						} else if (c.equals(MarkupExtension.class) && module instanceof MarkupExtension) {
							addMarkup((MarkupExtension) module);
						}
					}
				} else {
					error(new InstantiationException(), "Could not create instance of module of type %1$s.", clazz.getName());
				}
			}
		}

		addCustomModules();

		final String content = initContent();
		if (content != null) {
			this.content = accessor(content);
		} else {
			this.content = hasParent() ? parent.content : null;
		}

		if (clazz.isAnnotationPresent(XMLPackage.class)) {
			final XMLPackage ann = clazz.getAnnotation(XMLPackage.class);
			for (final String p : ann.value()) {
				packages.add(p);
			}
		}
		if (hasParent()) {
			packages.addAll(parent.packages);
		}
		addCustomPackages();

		if (clazz.isAnnotationPresent(XMLProperties.class)) {
			final XMLProperties ann = clazz.getAnnotation(XMLProperties.class);
			Field f = null;
			try {
				f = clazz.getDeclaredField(ann.value());
			} catch (final Exception e) {
				error(e, "Map %1$s not found in class %2$s.", ann.value(), clazz.getName());
			}
			properties = f;
		} else {
			properties = null;
		}

		addExtension(this);
	}

	private Parser(final Parser<T> parser, final Converter<T> custom) {
		clazz = parser.clazz;
		converter = custom;
		parent = parser.parent;
		metadata.putAll(parser.metadata);
		creators.putAll(parser.creators);
		extensions.addAll(parser.extensions);
		extensions.remove(parser);
		getters.addAll(parser.getters);
		markups.addAll(parser.markups);
		addExtension(this);
		content = parser.content;
		propertiesNamesMap = parser.propertiesNamesMap;
		properties = null;
	}

	@SuppressWarnings({ "rawtypes" })
	public final Parser<T> copy(final Converter custom) {
		if (custom == converter || custom == null) {
			return this;
		} else {
			return new Parser<T>(this, custom);
		}
	}

	protected Converter<T> createConverter() {
		return Converter.createDefaultConverter(clazz);
	}

	protected String initContent() {
		if (clazz.isAnnotationPresent(XMLContent.class)) {
			return clazz.getAnnotation(XMLContent.class).value();
		} else {
			return null;
		}
	}

	protected PropertiesNamesMap initPropertyNamesMap() {
		if (clazz.isAnnotationPresent(XMLNames.class)) {
			return Reflection.newInstance(clazz.getAnnotation(XMLNames.class).value());
		} else {
			return new PropertiesNamesMap.Identity();
		}
	}

	private <X> X createObjectFromClass(final Class<X> clazz, final boolean tryParameterless) {
		try {
			X inst = null;
			Constructor<X> c = Reflection.getConstructor(clazz, Parser.class);
			if (c != null) {
				inst = c.newInstance(this);
			} else {
				c = Reflection.getConstructor(clazz, Class.class);
				if (c != null) {
					inst = c.newInstance(this.clazz);
				} else if (tryParameterless) {
					inst = clazz.newInstance();
				}
			}
			return inst;
		} catch (final Exception e) {}
		return null;
	}

	private <X> List<X> createFromParentAndAnnotation(final MarkedSubrangeList<X> list, final Class<? extends X>[] ann,
			final Map<Class<?>, Object> map) {
		final List<X> res = new Vector<X>();
		X x;
		if (list != null) {
			for (final X p : list.getSpecialRange()) {
				final Class<?> cls = p.getClass();
				if (map.containsKey(cls)) {
					x = (X) map.get(cls);
				} else {
					x = (X) createObjectFromClass(cls, false);
					map.put(cls, x);
				}
				if (x != null) {
					res.add(x);
				} else {
					res.add(p);
				}
			}
		}
		if (ann != null) {
			for (final Class<? extends X> cls : ann) {
				if (map.containsKey(cls)) {
					x = (X) map.get(cls);
				} else {
					x = createObjectFromClass(cls, true);
					map.put(cls, x);
				}
				if (x != null) {
					res.add(x);
				}
			}
		}
		return res;
	}

	protected final <X extends XMLParserExtension> X addExtension(final X ext) {
		extensions.add(ext);
		return ext;
	}

	protected final <X extends MetadataParserExtension> X addMetadataExtension(final X meta) {
		metadata.put(meta.getBase(), meta);
		return meta;
	}

	protected final <X extends ObjectCreator<T>> X addCreator(final X creator) {
		creators.put((Class<? extends ObjectCreator<T>>) creator.getClass(), creator);
		return creator;
	}

	protected <X extends PropertyGetter> X addGetter(final X getter) {
		getters.add(getter);
		return getter;
	}

	protected <X extends MarkupExtension> X addMarkup(final X markup) {
		markups.add(markup);
		return markup;
	}

	protected void addCustomModules() {}

	protected void addCustomPackages() {}

	// ========================================================================================================================================================
	// utility stuff
	// ========================================================================================================================================================
	public class XMLParserFinder {

		private final String nodeTextContent;
		private final MarkupProperties props;
		private final MarkupExtension markup;
		private XMLParserExtension handler = null;
		private NodeNameInfo nodeInfo = null;

		public XMLParserFinder(final String nodeTextContent, final MarkupProperties props, final MarkupExtension markup) {
			this.nodeTextContent = nodeTextContent;
			this.props = props;
			this.markup = markup;
		}

		public XMLParserFinder(final String nodeTextContent) {
			this(nodeTextContent, null, null);
		}

		public MarkupProperties getProps() {
			return props;
		}

		public MarkupExtension getMarkup() {
			return markup;
		}

		public XMLParserExtension getHandler() {
			return handler;
		}

		public XMLParserExtension setHandler(final XMLParserExtension handler) {
			return this.handler = handler;
		}

		public NodeNameInfo getNodeInfo() {
			return nodeInfo;
		}

		public NodeNameInfo setNodeInfo(final NodeNameInfo nodeInfo) {
			return this.nodeInfo = nodeInfo;
		}

		public boolean isMarkup() {
			return props != null && markup != null;
		}

		public boolean isContent() {
			return handler == Parser.this;
		}

		public String getNodeTextContent() {
			return nodeTextContent;
		}
	}

	public final XMLParserFinder findMarkup(final String text, final T target) {
		if (BasicMarkupExtension.isMarkup(text)) {
			final MarkupProperties props = MarkupProperties.parseString(BasicMarkupExtension.getProperties(text));
			final String name = BasicMarkupExtension.getName(text);
			MarkupExtension markup = null;
			Parser<?> parser = this;
			if (Strings.nonEmpty(name)) {
				while (markup == null && parser != null) {
					final Iterator<MarkupExtension> ext = parser.markups.iterator();
					while (markup == null && ext.hasNext()) {
						final MarkupExtension m = ext.next();
						if (name.equals(m.getName()) && m.canHandle(target, props)) {
							markup = m;
						}
					}
					parser = parser.parent;
				}
			}
			return new XMLParserFinder(text, props, markup);
		} else {
			return new XMLParserFinder(text);
		}
	}

	protected final XMLParserFinder findHandler(final Node node, final ParsingInfo<T> info) {
		final XMLParserFinder finder = findMarkup(XML.getText(node), info.getTarget());
		boolean found = false;
		XMLParserExtension handler = null;
		Parser<?> parser = this;
		while (!found && parser != null) {
			final Iterator<XMLParserExtension> ext = parser.extensions.iterator();
			while (!found && ext.hasNext()) {
				handler = ext.next();
				if (found = finder.setNodeInfo(handler.parseNodeName(node, info)) != null) {
					finder.setHandler(handler);
					return finder;
				}
			}
			parser = parser.parent;
		}
		return null;
	}

	// ========================================================================================================================================================
	// parsing stuff
	// ========================================================================================================================================================

	public T convert(final String string, final ParsingInfo<T> info) {
		return converter.convert(string);
	}

	protected boolean customParseAttribute(final T target, final Node item, final ParsingInfo<T> info) {
		return false;
	}

	@SuppressWarnings({ "rawtypes" })
	protected boolean parseAttribute(final T target, final Node item, final ParsingInfo<T> info) {
		if (!customParseAttribute(target, item, info)) {
			final XMLParserFinder finder = findHandler(item, info);// TODO zamienić finder na ParsingInfo
			if (finder != null && target != null) {
				final NodeNameInfo nodeInfo = finder.getNodeInfo();
				final ParsingInfo newInfo = info.createChild((Parser<T>) nodeInfo.getParser(), nodeInfo, item);
				if (finder.isMarkup()) {
					return finder.getMarkup().parse(target, finder.getProps(), newInfo, finder.getHandler());
				} else {
					final Object obj = newInfo.getParser().convert(finder.getNodeTextContent(), newInfo);
					return finder.getHandler().setParsedProperty(target, newInfo, obj);
				}
			}
		} else {
			return true;
		}
		return false;
	}

	/**
	 * Sets all the attribute properties from the <code>node</code> into the <code>target</code> object. However, if the <code>target</code> is
	 * <code>null</code> - parses only the relevant attributes to aply the desired changes to the <code>node</code>. The latter is helpful for
	 * instance when it is desired to include outer file as a source of a map - then this simply should attach the nodes parsed from the outer file
	 * into the passed <code>node</code>.
	 * @param target the target element on which the propertiesh are to be set
	 * @param node the node containing all the desired attributes
	 * @param xmlns xml namespace retriever
	 * @return the metadata associated with the given <code>node</code>
	 */
	private T parseAttributes(final T target, final ParsingInfo<T> info) {
		final Element node = info.getElement();
		final NamedNodeMap attr = node.getAttributes();
		for (int i = 0; i < attr.getLength(); i++) {
			final Node item = attr.item(i);
			if (!parseAttribute(target, item, info)) {
				error(new Exception("XML syntax error"), "XML syntax error for name %1$s.", item.getNodeName());
			}
		}
		return target;
	}

	@SuppressWarnings({ "rawtypes" })
	private ParsingInfo contentInfo(final ParsingInfo info) {
		return info.createChild(content.getParser(),
				new NodeNameInfo(content.getPropertyName(), content.getParser().getClazz(), content.getParser()), null);
	}

	protected boolean customParseChildElement(final T target, final Element element, final ParsingInfo<T> info) {
		return false;
	}

	@SuppressWarnings({ "rawtypes" })
	protected boolean parseChildElement(final T target, final Element element, final ParsingInfo<T> info) {
		if (!customParseChildElement(target, element, info)) {
			final XMLParserFinder finder = findHandler(element, info);
			if (finder != null) {
				final Parser parser = finder.getNodeInfo().getParser();
				final ParsingInfo newInfo;
				if (finder.isContent()) {
					newInfo = contentInfo(info).createChild(parser, finder.getNodeInfo(), element);
				} else {
					newInfo = info.createChild(parser, finder.getNodeInfo(), element);
				}
				if (!parser.hasContent() && finder.isMarkup()) {
					return finder.getMarkup().parse(target, finder.getProps(), newInfo, finder.getHandler());
				} else {
					final Object value = parser.parse(newInfo);
					return finder.getHandler().setParsedProperty(target, newInfo, value);
				}
			}
		} else {
			return true;
		}
		return false;
	}

	@SuppressWarnings({ "rawtypes" })
	private T parseChildNodes(final T target, final ParsingInfo<T> info) {
		final Element node = info.getElement();
		final NodeList children = node.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			final Node child = children.item(i);
			switch (child.getNodeType()) {
				case Node.ELEMENT_NODE:
					parseChildElement(target, (Element) child, info);
				break;

				case Node.TEXT_NODE:
					if (hasContent()) {
						final XMLParserFinder finder = findMarkup(XML.getText(child), info.getTarget());
						final String txt = finder.getNodeTextContent();
						final Parser parser = content.getParser();
						if (finder.isMarkup()) {
							finder.getMarkup().parse(target, finder.getProps(), contentInfo(info), this);
						} else if (Strings.nonEmpty(txt)) {
							access(target, parser.convert(txt, info.createChild(parser, null, child)), info);
						}
					}
				break;
			}
		}
		return target;
	}

	@Override
	public T createInstance(final ParsingInfo<T> info) {
		final Node node = info.getNode();
		if (hasConverter() && (!hasContent() || !content.getParser().canConvert()) && node.getChildNodes().getLength() > 0) {
			final Node txt = node.getFirstChild();
			if (txt.getNodeType() == Node.TEXT_NODE) {
				final String string = XML.getText(txt);
				if (Strings.nonEmpty(string)) {
					node.removeChild(txt);
					return convert(string, info);
				}
			}
		}
		return null;
	}

	protected T newInstance(final ParsingInfo<T> info) {
		final Iterator<ObjectCreator<T>> creator = creators.valuesIterator();
		while (creator.hasNext()) {
			T instance = null;
			instance = creator.next().createInstance(info);
			if (instance != null) { return instance; }
		}
		return Reflection.newInstance(clazz);
	}

	private final void parseMetadata(final T target, final ParsingInfo<T> info) {
		for (final Metadata meta : Metadata.class.getEnumConstants()) {
			if (info.containsMetadata(meta) & metadata.containsKey(meta)) {
				metadata.get(meta).parseMetadata(target, info, info.getMetadata(meta));
			}
		}
	}

	private static final Map<Metadata, String> cutMetadata(final Element node) {
		final HashMap<Metadata, String> metadata = new HashMap<Metadata, String>();

		final Set<Node> remove = new HashSet<Node>();

		// first - cut attribute nodes
		final NamedNodeMap map = node.getAttributes();
		for (int i = 0; i < map.getLength(); i++) {
			final Node item = map.item(i);
			if (Metadata.is(item)) {
				final Metadata meta = Metadata.from(item);
				if (meta.isRemovable()) {
					remove.add(item);
				}
				metadata.put(meta, XML.getText(item));
			}
		}
		for (final Node item : remove) {
			node.removeAttributeNode((Attr) item);
		}

		// second - cut simple nodes
		remove.clear();
		final NodeList list = node.getChildNodes();
		for (int i = 0; i < list.getLength(); i++) {
			final Node item = list.item(i);
			if (Metadata.is(item)) {
				final Metadata meta = Metadata.from(item);
				if (meta.isRemovable()) {
					remove.add(item);
				}
				metadata.put(meta, XML.getText(item));
			}
		}
		for (final Node item : remove) {
			node.removeChild(item);
		}

		return metadata;
	}

	protected T parse(final ParsingInfo<T> info, final T target) {
		info.putAllMetadata(cutMetadata(info.getElement()));
		info.setTarget(target);
		parseMetadata(target, info);
		return parseChildNodes(parseAttributes(target, info), info);
	}

	public T parse(final ParsingInfo<T> info) {
		info.putAllMetadata(cutMetadata(info.getElement()));
		final T target = info.setTarget(newInstance(info));
		parseMetadata(target, info);
		return parseChildNodes(parseAttributes(target, info), info);
	}

	public final Class<T> getClazz() {
		return clazz;
	}

	public final boolean hasContent() {
		return content != null;
	}

	public final boolean canConvert() {
		return converter != null;
	}

	public final String getContent() {
		return content.getPropertyName();
	}

	public final boolean isContent(final String property) {
		return hasContent() && content.getPropertyName().equals(property);
	}

	public final boolean hasParent() {
		return parent != null;
	}

	@Override
	public final NodeNameInfo parseNodeName(final Node node, final ParsingInfo<?> info) {
		if (hasContent()) {
			final String clazz = NodeNameInfo.getFullClassName(node, info.getStaticInfo().copy(packages));
			if (clazz != null) { return new NodeNameInfo(content.getPropertyName(), clazz); }
		} else if (hasParent()) { return parent.parseNodeName(node, info); }
		return null;
	}

	@Override
	public final boolean setParsedProperty(final Object target, final ParsingInfo<?> info, final Object value) {
		if (isContent(info.getNodeInfo().getProperty())) {
			return content.access(target, value, info);
		} else if (hasParent()) { return parent.setParsedProperty(target, info, value); }
		return false;
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + " [clazz=" + clazz.getName() + "]";
	}

	@Override
	public final boolean access(final Object target, final Object value, final ParsingInfo<?> info) {
		if (hasContent()) {
			return content.access(target, value, info);
		} else if (hasParent()) { return parent.access(target, value, info); }
		return false;
	}

	@Override
	public final PropertySetter accessor(final String property) {
		PropertySetter accessor = null;
		for (final XMLParserExtension parser : extensions) {
			if (parser != this && (accessor = parser.accessor(property)) != null) { return accessor; }
		}
		if (hasParent()) {
			return parent.accessor(property);
		} else {
			return this;
		}
	}

	public final boolean hasConverter() {
		return converter != null;
	}

	@Override
	public final String getPropertyName() {
		return content != null ? content.getPropertyName() : null;
	}

	@Override
	public String getPropertyName(final String xmlname) {
		return propertiesNamesMap.getPropertyName(xmlname);
	}

	@Override
	public final Parser<?> getParser() {
		return this;
	}

	public final Converter<?> getConverter() {
		return converter;
	}

	public ObjectCreator<T> getCreator(final Class<? extends ObjectCreator<?>> cls) {
		return creators.get(cls);
	}

	public boolean hasNamedProperties() {
		return properties != null || hasParent() && parent.hasNamedProperties();
	}

	public void addNamedProperty(final Object target, final String name, final Object value) {
		if (properties != null) {
			final boolean accessible = properties.isAccessible();
			try {
				properties.setAccessible(true);
				Methods.put.invoke(properties.get(target), name, value);
			} catch (final Exception e) {
				error(e, "Error trying to add named property object.");
			} finally {
				properties.setAccessible(accessible);
			}
		} else if (hasParent()) {
			parent.addNamedProperty(target, name, value);
		}
	}

	public PropertyGetter getPropertyGetter(final String property) {
		for (final PropertyGetter get : getters) {
			if (get.getPropertyType(property) != null) { return get; }
		}
		if (hasParent()) {
			return parent.getPropertyGetter(property);
		} else {
			return null;
		}
	}

	@Override
	public Class<?> getPropertyType(final String property) {
		final PropertyGetter getter = getPropertyGetter(property);
		return getter != null ? getter.getPropertyType(property) : null;
	}

	@Override
	public Object getPropertyValue(final Object target, final String property) {
		return getPropertyGetter(property).getPropertyValue(target, property);
	}
}
